package org.darkforest.dao.interceptor;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.darkforest.common.logger.Logger;
import org.darkforest.common.util.Page;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * interceptor for pageable sql
 * Created by Justin on 15/2/7.
 */

@Intercepts(
        {@Signature(
                type = Executor.class,
                method = "query",
                args = {
                        MappedStatement.class,
                        Object.class,
                        RowBounds.class,
                        ResultHandler.class
                }
        )}
)
public class PageablePrepareInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Logger.debug(PageablePrepareInterceptor.class, "Preparing pageable interceptor.");
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Logger.debug(PageablePrepareInterceptor.class, "Get parameters. {}", parameter);

        if (!(parameter instanceof Page)) {
            Logger.debug(PageablePrepareInterceptor.class, "Parameter is not pageable. Passing to other interceptors.");
            return invocation.proceed();
        }
        Page page = (Page) parameter;
        int rowCount = fetchRowCount(mappedStatement, parameter);

        if (page.getPageSwitch()) {

            if(!page.isQueryRelation()){
                page.setCount(rowCount);
                String newSql = String.format("%s LIMIT %d, %d", boundSql.getSql().trim(), page.getStartNumber(), page.getPageSize());
                Logger.debug(PageablePrepareInterceptor.class, "Build paging SQL finished. SQL: {}", newSql);
                BoundSql pageSql = cloneBoundSql(mappedStatement, boundSql, newSql);
                MappedStatement newMappedStatement = cloneMappedStatement(mappedStatement, new BoundSqlSqlSource(pageSql));
                invocation.getArgs()[0] = newMappedStatement;
                invocation.getArgs()[1] = page;
            }else{
                //sql关联查询 并且需要分页 调用次方法 在需要插入limit位置 填写 “~PWR” limit就会在此处
                page.setCount(rowCount);
                String newSql = boundSql.getSql().trim().replace("PWR",String.format("LIMIT %d, %d", page.getStartNumber(), page.getPageSize()));
                Logger.debug(PageablePrepareInterceptor.class, "Build paging SQL finished. SQL: {}", newSql);
                BoundSql pageSql = cloneBoundSql(mappedStatement, boundSql, newSql);
                MappedStatement newMappedStatement = cloneMappedStatement(mappedStatement, new BoundSqlSqlSource(pageSql));
                invocation.getArgs()[0] = newMappedStatement;
                invocation.getArgs()[1] = page;
            }


        }



        return invocation.proceed();
    }

    /**
     * fetch prepare sql count
     *
     * @param mappedStatement mapped statement
     * @param parameter       parameters
     * @return row count
     */
    private int fetchRowCount(MappedStatement mappedStatement, Object parameter) throws SQLException {
        // build sql
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
//        String countSql = String.format("SELECT COUNT(1) FROM (%s) cnt", boundSql.getSql().trim().replace("PWR" ,""));
        String countSql = String.format("SELECT COUNT(1) FROM (%s) cnt", boundSql.getSql());
        if (boundSql.getSql().trim().contains("PWR")) {
            countSql = String.format("SELECT COUNT(distinct sp_id) FROM (%s) cnt", boundSql.getSql().trim().replace("PWR" ,""));
        }
        Logger.debug(PageablePrepareInterceptor.class, "Build count SQL done. SQL : {}", countSql);

        Connection connection = mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection();
        BoundSql countBoundSql = cloneBoundSql(mappedStatement, boundSql, countSql);
        PreparedStatement statement = connection.prepareStatement(countSql);
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameter, countBoundSql);
        parameterHandler.setParameters(statement);
        ResultSet resultSet = statement.executeQuery();
        int rowCount = resultSet.next() ? resultSet.getInt(1) : 0;
        resultSet.close();
        statement.close();
        connection.close();

        return rowCount;
    }

    private BoundSql cloneBoundSql(MappedStatement mappedStatement, BoundSql boundSql, String sql) {
        BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(),
                boundSql.getParameterObject());
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }
        return newBoundSql;
    }

    private MappedStatement cloneMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        Builder builder = new Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());

        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        builder.keyProperty(mergeKeyProperties(ms.getKeyProperties()));
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }

    private String mergeKeyProperties(String[] properties) {
        if (properties == null || properties.length == 0) {
            return StringUtils.EMPTY;
        }
        StringBuilder resultBuilder = new StringBuilder();

        for (String property : properties) {
            resultBuilder.append(",").append(property);
        }
        /**
         * the build string format is ,%s,%s,%s
         * to avoid the first comma character, returns the substring(1..n) of substring
         */
        return resultBuilder.toString().substring(1);
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    public class BoundSqlSqlSource implements SqlSource {
        BoundSql boundSql;
        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}
